package com.hdu.xxlJobLocalCacheFix.job;

import com.github.benmanes.caffeine.cache.Cache;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.hdu.xxlJobLocalCacheFix.enums.OpType;
import com.hdu.xxlJobLocalCacheFix.holder.ApplicationContextHolder;
import com.hdu.xxlJobLocalCacheFix.req.LocalCacheFixReq;
import com.hdu.xxlJobLocalCacheFix.res.LocalCacheFixRes;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.val;
import org.springframework.beans.BeansException;

import java.util.function.Function;

import static com.alibaba.fastjson.JSON.*;
import static com.hdu.xxlJobLocalCacheFix.enums.OpType.*;
import static com.hdu.xxlJobLocalCacheFix.res.LocalCacheFixRes.fail;
import static com.hdu.xxlJobLocalCacheFix.res.LocalCacheFixRes.success;
import static com.xxl.job.core.context.XxlJobHelper.log;
import static java.lang.String.format;
import static java.util.Objects.nonNull;
import static java.util.Optional.ofNullable;

public class LocalCacheFixJob {

    private static final String JOB_NAME = "localCacheFixJob";
    private static final String NO_SUCH_CACHE_INFO_TEMPLATE = "no such cache : %s";
    private static final String UNSUPPORTED_OP_TYPE_INFO_TEMPLATE = "unsupported op type : %s";
    private static final String INVALID_CLASS_TYPE_INFO_TEMPLATE = "cacheName : %s, invalid class type : %s";

    private static final String CLEAR_SUCCESS_INFO_TEMPLATE = "clear success, cacheName : %s";

    private static final String LIST_ALL_SUCCESS_INFO_TEMPLATE =
            "list all success, cacheName : %s, cache content : \n %s";
    private static final String LIST_ALL_FAIL_INFO_TEMPLATE =
            "list all fail, cacheName : %s. because this cache no any content";

    private static final String UPDATE_SUCCESS_INFO_TEMPLATE =
            "update success,cacheName : %s,  cacheKey : %s, cacheKeyType : %s, " +
                    "oldCacheValue : %s, oldCacheValueType : %s , newCacheValue : %s, newCacheValueType : %s";
    private static final String UPDATE_FAIL_INFO_TEMPLATE =
            "update fail,cacheName : %s,  cacheKey : %s, cacheKeyType : %s. because cacheKey : %s doesn't exist";

    private static final String PUT_SUCCESS_INFO_TEMPLATE =
            "put success,cacheName : %s,  cacheKey : %s, cacheKeyType : %s, cacheValue : %s, cacheValueType : %s";
    private static final String PUT_FAIL_INFO_TEMPLATE =
            "put fail,cacheName : %s,  cacheKey : %s, cacheKeyType : %s, cacheValue : %s, cacheValueType : %s. " +
                    "because cacheKey : %s already exist";

    private static final String GET_SUCCESS_INFO_TEMPLATE =
            "get success,cacheName : %s,  cacheKey : %s, cacheKeyType : %s, cacheValue : %s, cacheValueType : %s";
    private static final String GET_FAIL_INFO_TEMPLATE =
            "get fail, cacheName : %s,  cacheKey : %s, cacheKeyType : %s. because cache key : %s doesn't exist";

    private static final String DELETE_SUCCESS_INFO_TEMPLATE =
            "delete success,cacheName : %s,  cacheKey : %s, cacheKeyType : %s, cacheValue : %s, cacheValueType : %s";
    private static final String DELETE_FAIL_INFO_TEMPLATE =
            "delete fail, cacheName : %s,  cacheKey : %s, cacheKeyType : %s. because cache key : %s doesn't exist";


    private final ImmutableMap<OpType, Function<LocalCacheFixReq, LocalCacheFixRes>> OP_MAP = ImmutableMap.of(
            DELETE, this::delete,
            PUT, this::put,
            UPDATE, this::update,
            LIST_ALL, this::listAll,
            CLEAR, this::clear,
            GET, this::get
    );

    @XxlJob(JOB_NAME)
    public void localCacheFix(String params) {
        val localCacheFixReq = parseObject(params, LocalCacheFixReq.class);
        val opType = localCacheFixReq.getOpType();
        if (nonNull(opType)) {
            ofNullable(OP_MAP.get(opType))
                    .ifPresent(opFunction -> log(toJSONString(opFunction.apply(localCacheFixReq))));
        } else {
            ofNullable(parseObject(params))
                    .flatMap(paramJsonObj -> ofNullable(paramJsonObj.getString("opType")))
                    .ifPresent(opTypeStr -> log(format(UNSUPPORTED_OP_TYPE_INFO_TEMPLATE, opTypeStr)));
        }
    }

    private LocalCacheFixRes delete(LocalCacheFixReq localCacheFixReq) {
        val cacheName = localCacheFixReq.getCacheName();
        val cacheKey = localCacheFixReq.getCacheKey();
        val cacheKeyType = localCacheFixReq.getCacheKeyType();
        try {
            val cache = ApplicationContextHolder.getBean(cacheName, Cache.class);
            val key = getObj(cacheKey, cacheKeyType);
            val cacheValue = cache.getIfPresent(key);
            return ofNullable(cacheValue)
                    .map(v -> {
                        cache.invalidate(key);
                        return success(
                                format(
                                        DELETE_SUCCESS_INFO_TEMPLATE,
                                        cacheName,
                                        cacheKey,
                                        cacheKeyType,
                                        cacheValue,
                                        cacheValue.getClass().getSimpleName()
                                )
                        );
                    })
                    .orElseGet(() -> fail(
                                    format(
                                            DELETE_FAIL_INFO_TEMPLATE,
                                            cacheName,
                                            cacheKey,
                                            cacheKeyType,
                                            cacheKey
                                    )
                            )
                    );
        } catch (BeansException | ClassNotFoundException e) {
            if (e instanceof BeansException) {
                return fail(format(NO_SUCH_CACHE_INFO_TEMPLATE, cacheName));
            } else {
                return fail(format(INVALID_CLASS_TYPE_INFO_TEMPLATE, cacheName, e.getMessage()));
            }
        }
    }


    private LocalCacheFixRes get(LocalCacheFixReq localCacheFixReq) {
        val cacheName = localCacheFixReq.getCacheName();
        val cacheKey = localCacheFixReq.getCacheKey();
        val cacheKeyType = localCacheFixReq.getCacheKeyType();
        try {
            val cache = ApplicationContextHolder.getBean(cacheName, Cache.class);
            val key = getObj(cacheKey, cacheKeyType);
            val cacheValue = cache.getIfPresent(key);
            return ofNullable(cacheValue)
                    .map(v -> success(
                            format(
                                    GET_SUCCESS_INFO_TEMPLATE,
                                    cacheName,
                                    cacheKey,
                                    cacheKeyType,
                                    cacheValue,
                                    cacheValue.getClass().getSimpleName()
                            )
                    ))
                    .orElseGet(() -> fail(
                            format(
                                    GET_FAIL_INFO_TEMPLATE,
                                    cacheName,
                                    cacheKey,
                                    cacheKeyType,
                                    cacheKey
                            )
                    ));
        } catch (BeansException | ClassNotFoundException e) {
            if (e instanceof BeansException) {
                return fail(format(NO_SUCH_CACHE_INFO_TEMPLATE, cacheName));
            } else {
                return fail(format(INVALID_CLASS_TYPE_INFO_TEMPLATE, cacheName, e.getMessage()));
            }
        }
    }

    private LocalCacheFixRes clear(LocalCacheFixReq localCacheFixReq) {
        val cacheName = localCacheFixReq.getCacheName();
        try {
            val cache = ApplicationContextHolder.getBean(cacheName, Cache.class);
            cache.invalidateAll();
            return success(format(CLEAR_SUCCESS_INFO_TEMPLATE, cacheName));
        } catch (BeansException e) {
            return fail(format(NO_SUCH_CACHE_INFO_TEMPLATE, cacheName));
        }
    }

    @SuppressWarnings("unchecked")
    private LocalCacheFixRes update(LocalCacheFixReq localCacheFixReq) {
        val cacheName = localCacheFixReq.getCacheName();
        val cacheKey = localCacheFixReq.getCacheKey();
        val cacheKeyType = localCacheFixReq.getCacheKeyType();
        val cacheValue = localCacheFixReq.getCacheValue();
        val cacheValueType = localCacheFixReq.getCacheValueType();
        try {
            val cache = ApplicationContextHolder.getBean(cacheName, Cache.class);
            Object cacheValueObj = getObj(cacheValue, cacheValueType);
            val cacheKeyObj = getObj(cacheKey, cacheKeyType);
            return ofNullable(cache.getIfPresent(cacheKeyObj))
                    .map(oriV -> {
                        cache.put(cacheKeyObj, cacheValueObj);
                        return success(
                                format(
                                        UPDATE_SUCCESS_INFO_TEMPLATE,
                                        cacheName,
                                        cacheKey,
                                        cacheKeyType,
                                        oriV,
                                        oriV.getClass().getSimpleName(),
                                        cacheValue,
                                        cacheValueType
                                )
                        );
                    }).orElseGet(() -> fail(
                            format(
                                    UPDATE_FAIL_INFO_TEMPLATE,
                                    cacheName,
                                    cacheKey,
                                    cacheKeyType,
                                    cacheKey
                            )
                    ));
        } catch (BeansException | ClassNotFoundException e) {
            if (e instanceof BeansException) {
                return fail(format(NO_SUCH_CACHE_INFO_TEMPLATE, cacheName));
            } else {
                return fail(format(INVALID_CLASS_TYPE_INFO_TEMPLATE, cacheName, e.getMessage()));
            }
        }
    }

    @SuppressWarnings("unchecked")
    private LocalCacheFixRes put(LocalCacheFixReq localCacheFixReq) {
        val cacheName = localCacheFixReq.getCacheName();
        val cacheKey = localCacheFixReq.getCacheKey();
        val cacheKeyType = localCacheFixReq.getCacheKeyType();
        val cacheValue = localCacheFixReq.getCacheValue();
        val cacheValueType = localCacheFixReq.getCacheValueType();
        try {
            val cache = ApplicationContextHolder.getBean(cacheName, Cache.class);
            val cacheValueObj = getObj(cacheValue, cacheValueType);
            val cacheKeyObj = getObj(cacheKey, cacheKeyType);
            return ofNullable(cache.getIfPresent(cacheKeyObj))
                    .map(oriV -> fail(
                            format(
                                    PUT_FAIL_INFO_TEMPLATE,
                                    cacheName,
                                    cacheKey,
                                    cacheKeyType,
                                    cacheValue,
                                    cacheValueType,
                                    cacheKey
                            )
                    ))
                    .orElseGet(() -> {
                        cache.put(cacheKeyObj, cacheValueObj);
                        return success(
                                format(
                                        PUT_SUCCESS_INFO_TEMPLATE,
                                        cacheName,
                                        cacheKey,
                                        cacheKeyType,
                                        cacheValue,
                                        cacheValueType
                                )
                        );
                    });
        } catch (BeansException | ClassNotFoundException e) {
            if (e instanceof BeansException) {
                return fail(format(NO_SUCH_CACHE_INFO_TEMPLATE, cacheName));
            } else {
                return fail(format(INVALID_CLASS_TYPE_INFO_TEMPLATE, cacheName, e.getMessage()));
            }
        }
    }

    @SuppressWarnings("unchecked")
    private LocalCacheFixRes listAll(LocalCacheFixReq localCacheFixReq) {
        val cacheName = localCacheFixReq.getCacheName();
        try {
            val cache = ApplicationContextHolder.getBean(cacheName, Cache.class);
            val cacheMap = cache.asMap();
            if (cacheMap.isEmpty()) {
                return fail(format(LIST_ALL_FAIL_INFO_TEMPLATE, cacheName));
            }
            val joiner = Joiner.on("\n");
            val singleCacheInfo = new StringBuilder();
            val cacheInfoList = Lists.newArrayList();
            cacheMap.forEach((cacheKey, cacheValue) -> {
                singleCacheInfo.setLength(0);
                singleCacheInfo
                        .append("cacheKey : ").append(cacheKey)
                        .append("cacheValue : ").append(cacheValue)
                        .append(
                                format(
                                        "(cacheKeyType : %s, cacheValueType : %s)",
                                        cacheKey.getClass().getSimpleName(),
                                        cacheValue.getClass().getSimpleName()
                                )
                        );
                cacheInfoList.add(singleCacheInfo.toString());
            });
            val cacheInfoStr = joiner.join(cacheInfoList);
            return success(
                    format(
                            LIST_ALL_SUCCESS_INFO_TEMPLATE,
                            cacheName,
                            cacheInfoStr
                    )
            );
        } catch (BeansException e) {
            return fail(format(NO_SUCH_CACHE_INFO_TEMPLATE, cacheName));
        }
    }

    private Object getObj(String objStr, String objType) throws ClassNotFoundException {
        Class<?> objClass = Class.forName(objType);
        return parseObject(objStr, objClass);
    }
}
